﻿//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;

using King.Extensions;
using King.Reflection;
using King.Infoset.Schema;
using King.Infoset.Reflection;
using King.Infoset.Entity;

namespace King.Infoset.Dispatch {

    public static class KingDispatch {

        #region Static Public Members
        public static object ResolveEntityRefs(object argument, Func<object, object> resolveClientId) {
            if (argument == null)
                return null;
            
            var result = VisitorNode.Transform(argument, delegate(VisitorNode node) {
                if (node.Value != null && 
                    node.Member != null && 
                    node.Member.IsEntityRefId())
                    return resolveClientId(node.Value);

                return node.ShallowCopyValue();
            });

            return result;
        }
        public static IEnumerable<object> GetServerEntityRefs(object source) {
            var result =
                from o in VisitorNode.Traverse(source, o => o.Contract.IsEntityRef())
                where o.Value != null
                select o.Value;
            return result.ToArray();
        }
        public static XsOperation BindOperation(
            XsContract contract, string operationName, ref object entity,
            ref IEnumerable<object> arguments) {

            contract = XsLoader.GetContractOrNull(entity) ?? contract;
            if (contract == null)
                throw new ArgumentException("Operation binding failed.");

            if (contract.IsEntityRef())
                contract = contract.GetDeRefContract();

            // find all methods with a matching name and IsStatic
            var isStatic = entity == null;
            var overloads = 
               (from o in contract.Operations((XName)operationName)
                where o.IsStatic == isStatic
                select o.MethodInfo).ToArray();

            // handle instance case
            if (!isStatic) {
                // verify the argument is an entityref
                var refContract = XsLoader.GetContract(entity);
                if (!refContract.IsEntityRef())
                    throw new ArgumentException("First argument to non-static operation must be an EntityRef.");

                // convert to entity                
                var id = refContract.GetId(entity);
                entity = refContract.GetDeRefContract().DeserializedType.Activate();
                contract.SetId(entity, id);

                // and that the first argument can be this
                if (!contract.MetadataType.IsAssignableFrom(entity.GetType()))
                    return null;
            }

            // convert IEnumerable<T> to T[] to support co-variant binding
            var objectArguments = NormalizeArguments(arguments);

            // bind to any visible method
            var bindingFlags = BindingFlags.NonPublic | BindingFlags.Public;
            bindingFlags |= isStatic ? BindingFlags.Static : BindingFlags.Instance;

            // bind to method and munge arguments to handle params case
            object dummy;
            var methodInfo = Type.DefaultBinder.BindToMethod(
                bindingFlags,
                overloads,
                ref objectArguments,
                new ParameterModifier[] { }, 
                null, null, out dummy);

            arguments = objectArguments;
            return contract.GetOperation(methodInfo);
        }
        public static object InvokeOperation(XsOperation operation, object entity, IEnumerable<object> arguments) {
            var result = operation.MethodInfo.Invoke(entity, arguments.ToArray());
            var enumeration = result as IEnumerable;
            if (enumeration != null && enumeration.ToEnumerableOfObject().None())
                return null;
            return result;
        }
        public static object ProcessUpdateResult(object argument, object result) {
            // return any values that were submitted and have changed after refresh (e.g. ids, cc#'s)
            return VisitorNode.LeftCopy(
                argument,                           // request (left); all sent values considered
                result,                             // result (right)
                (l, r) => ShouldCopy(l, r, true),
                (l, r) => ShouldCopy(l, r, false),
                (l, r) => r);                       // copy refreshed value
        }
        public static object ProcessCreateResult(object argument, object result) {
            // return any values that have changed (e.g. ids, cc#'s, AdTags)
            return VisitorNode.LeftCopy(
                result,                             // result (left); all refreshed values considered
                argument,                           // request (right)
                (l, r) => ShouldCopy(r, l, true),
                (l, r) => ShouldCopy(r, l, false),
                (l, r) => l);                       // copy refreshed value
        }
        public static object ProcessRefreshResult(object result) {
            // supress default values
            return VisitorNode.Copy(result,
                o => ShouldReturn(o, true),
                o => ShouldReturn(o, false));
        }
        #endregion

        #region Static Private Members
        private static object[] NormalizeArguments(IEnumerable<object> arguments) {
            // Transform enumeration arguments to arrays. The type of the array should be
            // the greatest common base type of its elements. We use arrays because they
            // are covariant and so bind better with our methods. e.g. An array of Foo[]
            // will bind with a signature of object[] where as IEnumerable<Foo> will not
            // bind with IEnumerable<object>. IEnumerables are covariant in C# 4.0.

            var result = new List<object>();
            foreach (var argument in arguments) {
                var enumeration = argument as IEnumerable<object>;
                if (enumeration == null) {
                    // argument is not an enumeration so just copy it
                    result.Add(argument);

                } else {
                    // argument is an enumeration so discover the GCBT and create a new array
                    var gcb = enumeration.Select(o => o.GetType()).GreatestCommonBaseType();
                    if (gcb != null) {
                        var array = Array.CreateInstance(gcb, enumeration.Count());
                        Array.Copy(enumeration.ToArray(), array, array.Length);
                        result.Add(array);
                    }
                }
            }

            return result.ToArray();
        }
        private static bool ShouldReturn(VisitorNode node, bool hasChildren) {
            return ShouldCopy(null, node, hasChildren);
        }
        private static bool ShouldCopy(VisitorNode? request, VisitorNode response, bool hasChildren) {
            // null values, empty strings and empty enumerations are never copied
            if (response.ValueIsEmpty)
                return false;

            // do not return internal nodes if not returning any of it's children
            if (response.ValueCanHaveChildren && !hasChildren)
                return false;

            // do not return values that have not changed
            if (request != null) {
                var requestValue = request.Value.Value;
                var responseValue = response.Value;
                if (object.Equals(requestValue, responseValue))
                    return false;
            }

            // copy if there's no ContractMember (e.g. element in an enumeration)
            if (response.Member == null)
                return true;

            // supress nodes we never return (password, securityAnswer, etc)
            if (response.Member.IsDefined<NeverReturnedAttribute>())
                return false;

            // supress defaults if HideDefault = true
            if (response.Member.IsHideDefault()) {
                var defaultValue = response.Member.DeserializedDefaultValue();
                return !object.Equals(response.Value, defaultValue);
            }

            // value has changed and we are not supressing the diff so copy
            return true;
        }
        #endregion
    }
}